// tspcluster - Traveling Salesman Problem
// Copyright (C) 2006  Frederik Carlier
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

using System;
using System.IO;
using System.Globalization;

namespace UGent.Tsp
{
    /// <summary>
    /// Represents a probability distribution.
    /// </summary>
    [Serializable()]
    public class Distribution
    {
#region Constructors
        /// <summary>
        /// Creates a flat distribution
        /// </summary>
        public Distribution(double lower, bool lowerExtended, double upper, bool upperExtended, int count)
        {
            this.lower = lower;
            this.upper = upper;
            this.count = count;
            this.lowerExtended = lowerExtended;
            this.upperExtended = upperExtended;
            this.range = upper - lower;
            this.interval = range / (double)count;
            this.values = new double[count];
            for (int i = 0; i < count; i++)
                values[i] = 1.0 / (double)count;
            this.isNormalized = true;
        }

        [CLSCompliant(false)]
        public Distribution(double lower, bool lowerExtended, double upper, bool upperExtended, int count, double[] values)
        {
            this.lower = lower;
            this.upper = upper;
            this.count = count;
            this.lowerExtended = lowerExtended;
            this.upperExtended = upperExtended;
            this.range = upper - lower;
            this.interval = range / (double)count;
            this.values = values;
        }
        
        [CLSCompliant(false)]
        public Distribution(double lower, bool lowerExtended, double upper, bool upperExtended, int count, long[] values)
        {
            this.lower = lower;
            this.upper = upper;
            this.count = count;
            this.lowerExtended = lowerExtended;
            this.upperExtended = upperExtended;
            this.range = upper - lower;
            this.interval = range / (double)count;
            this.values = new double[values.Length];
            values.CopyTo(this.values, 0);
        }
        #endregion

        #region Fields
        private double[] values;
        private double lower;
        private bool lowerExtended;
        private double upper;
        private bool upperExtended;
        private double interval;
        private int count;
        private double range;
        private bool isNormalized;
        #endregion

        #region Properties
        public double Lower
        {
            get { return lower; }
        }

        public double Upper
        {
            get { return upper; }
        }

        public int Count
        {
            get { return count; }
        }

        public double Interval
        {
            get { return interval; }
        }

        public double Range
        {
            get { return range; }
        }

        [CLSCompliant(false)]
        public double[] Values
        {
            get { return values; }
        }
        
        public double this[int i]
        {
            get { return values[i]; }
            set { values[i] = value; }
        }
        #endregion 

        #region Methods
        public void WriteToStream(Stream stream)
        {
            using (StreamWriter writer = new StreamWriter(stream))
            {
                double binValue = lower + interval / 2;
                
                for (int i = 0; i < count; i++)
                {
                    writer.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0}, {1}", binValue, values[i]));
                    binValue += interval;
                }
            }
        }

        public double GetValue(double value)
        {
            return this.values[GetBinIndex(value)];
        }

        public int GetBinIndex(double value)
        {
            if ((!lowerExtended && value < lower) || (!upperExtended && value >= upper))
                throw new ArgumentOutOfRangeException("value", "value must be greater than or equal to lower and less than upper");

            if (value < lower)
                return 0;

            if (value >= upper)
                return count - 1;

            // Get the distance between the lower bound of the histogram
            // and the value;
            // In the example: relativeValue = 54 - 10 = 44
            double relativeValue = value - lower;

            // Divide by the interval of the bin, to get the bin number
            // in a real value (ex: 3.14)
            // In the example: binPosition = 44/10 = 4.4
            double binPosition = relativeValue / interval;

            // Use the floor as the real bin number
            // In the example: binNumber = 4
            int binIndex = (int)Math.Floor(binPosition);

            return binIndex;
        }

        public void AddExtraTick(double value)
        {
            for (int i = 0; i < count; i++)
            {
                if(values[i] == 0)
                    values[i] = value;
            }
        }
        
        public void WeighExponential(double beta)
        {
            double binValue = lower + interval / 2;
            for (int i = 0; i < count; i++)
            {
                binValue += interval;
                if(values[i] != 0)
                {
                    values[i] *= Math.Exp(-1.0*beta*binValue);
                }
            }
        }

        public void Normalize()
        {
            if (isNormalized)
                return;

            double sum = 0;
            for (int i = 0; i < values.Length; i++)
                sum += values[i];

            for (int i = 0; i < values.Length; i++)
                values[i] = values[i] / sum;

            isNormalized = true;
        }

        public void Multiply(Distribution value)
        {
            Multiply(value, true);
        }

        /// <summary>
        /// Multiplies the probabilities of this distribution with the probabilities of another
        /// distribution.
        /// </summary>
        /// <param name="value">The distribution to multiply with.</param>
        /// <param name="multiplyZeros">If set to false, the multiplication will not be done
        /// if a value of the new distribution is zero.</param>
        public void Multiply(Distribution value, bool multiplyZeros)
        {
            if (value == null)
                throw new ArgumentNullException("value");

            if (value.count != count)
                throw new ArgumentOutOfRangeException("value", "Dimension does not match.");

            for (int i = 0; i < count; i++)
            {
                if(multiplyZeros || value.values[i] != 0)
                    this.values[i] *= value.values[i];
            }

            this.isNormalized = false;
        }

        public Distribution Clone()
        {
            return new Distribution(lower, lowerExtended, upper, upperExtended, count, (double[])values.Clone());
        }

        public Distribution GetInverse()
        {
            Distribution inverse = new Distribution(lower, lowerExtended, upper, upperExtended, count, (double[])values.Clone());
            double minimumProbability = double.MaxValue;
            for(int i = 0; i < count; i++)
                if(values[i] > 0 && values[i] < minimumProbability)
                    minimumProbability = values[i];
                    
            for (int i = 0; i < count; i++)
            {
                if (inverse.values[i] == 0)
                    inverse.values[i] = 1.0 / minimumProbability;
                else
                    inverse.values[i] = 1.0 / inverse.values[i];
            }
            return inverse;
        }
        
        public override string ToString()
        {
            return values.ToString();
        }
        #endregion
    }
}